0.6+

Introduction

PyloaderV 0.6+ lets you write GTA V mods in real Python 3.12. Drop a .py file into the pyscript/ folder, launch the game, and your script runs alongside ScriptHookV.

CPython 3.12 embedded High-level API 6649 raw natives

✨ Key Benefits
  • No Python install needed — the runtime is bundled
  • Real Python ecosystem — f-strings, dataclasses, asyncio, pip packages
  • Hot-reload — press F9 in-game to reload your scripts
  • Two API layers — idiomatic wrappers (gta.player, gta.world...) plus 6649 raw natives

Installation

Requirements

  1. GTA V (Legacy or Enhanced)
  2. Install ScriptHookV (provides ScriptHookV.dll + dinput8.dll)
  3. Download pyloaderv-0.7.zip and copy the contents to your GTA V root

Folder Structure

📁 Grand Theft Auto V
📄 GTA5.exe
📄 ScriptHookV.dll ← from ScriptHookV
📄 dinput8.dll
📄 PyloaderV.asi ← the loader
⚙️ PyloaderV.ini ← configuration
📁 pyscript ← your Python scripts go here
🐍 hello.py
📁 Lib ← shared modules (optional)
🐍 my_helpers.py
📁 shvpy_runtime ← embedded Python 3.12 (do not rename)
📁 pyscript layout

Files directly under pyscript/ are loaded as scripts. Files inside a folder named Lib (case-insensitive) are not loaded as entry points, so you can use them as shared modules: from my_helpers import helper.

Your First Script

Create hello.py in pyscript/:

import gta

INTERVAL_MS = 1000            # on_tick runs every second

def on_start():
    gta.notify("~g~Hello from Python!")
    gta.log("info", "hello.py started")

def on_tick():
    gta.log("info", "tick")

def on_key_down(vk, down, shift, ctrl, alt):
    if down and vk == 0x70:   # F1
        gta.notify("~b~F1 pressed")

def on_aborted():
    gta.log("info", "hello.py stopped")

Launch the game. After ScriptHookV is ready you should see the green Hello from Python! ticker. Press F9 anytime to reload all scripts without restarting.

Script Structure

A PyloaderV script can define any of these optional functions and module-level variables:

# Optional: how often on_tick runs, in milliseconds
INTERVAL_MS = 100          # 0 = every frame

# Optional: load order. Lower = earlier.
__priority__ = 100

def on_start():
    """Called once when the script is loaded."""
    pass

def on_tick():
    """Called every INTERVAL_MS ms (or every frame if 0).
    You can call gta.wait(ms) here to pause."""
    pass

def on_key_down(vk, down, shift, ctrl, alt):
    """Keyboard event. Do NOT call gta.wait() here."""
    pass

def on_aborted():
    """Called before reload or shutdown.
    Use it to clean up entities, blips, custom cameras, etc."""
    pass

Callbacks

Callback When it runs Can call gta.wait()?
on_start()Once, after the script is loadedNo
on_tick()Every INTERVAL_MS msYes
on_key_down(vk, down, shift, ctrl, alt)On keyboard eventsNo
on_aborted()Before reload / shutdownNo

Module-level options

VariableTypeDescription
INTERVAL_MSintTick period in ms. 0 = every frame.
__priority__intLoad and dispatch order; lower = earlier (default 100).
ℹ️ Note
All callbacks are optional — only define what you need. A script with just on_key_down is perfectly valid.

Imports

The PyloaderV API lives entirely under the gta module.

# Built-in module (always available inside the game)
import gta

# High-level wrappers
from gta import player, world, ui
from gta import entity, ped, vehicle
from gta import math as gmath

# Wrapper classes
from gta.entity  import Entity
from gta.ped     import Ped
from gta.vehicle import Vehicle
from gta.player  import Player

# Raw natives (45 namespaces, only import what you need)
from gta.natives.player import PLAYER_PED_ID
from gta.natives.misc   import SET_TIME_SCALE
from gta.natives.fire   import ADD_EXPLOSION

# The standard library is also available
import random
import time
from dataclasses import dataclass

Player

Basic access

from gta import player as gplayer

me = gplayer.current()       # Player object
ped = me.ped                  # the controlled Ped

Properties

MemberDescription
me.idPlayer slot, usually 0.
me.pedThe Ped the player is controlling.
me.wanted_levelRead or write 0..5.
me.is_deadRead-only bool.
me.set_police_ignore(True)Cops ignore you.
from gta import player, ui

def on_tick():
    me = player.current()
    if me.wanted_level >= 5:
        ui.notify("~r~5 stars!")

def on_key_down(vk, down, shift, ctrl, alt):
    if down and vk == 0x71:    # F2
        player.current().wanted_level = 0

Peds

Spawning

from gta import world, player

origin = player.current().ped.position
soldier = world.create_ped("s_m_y_marine_01", origin, ped_type=29)
if soldier:
    soldier.give_weapon(0xFAD1F743, ammo=999)   # carbine rifle
    soldier.task_combat(player.current().ped)

create_ped takes a model name or hash, a position (x, y, z), an optional heading (degrees) and a ped_type (4 = civ male, 26 = civ default, 29 = military, 30 = cop).

Ped API

MemberDescription
ped.give_weapon(weapon_hash, ammo=999, hidden=False, equip_now=True)Give a weapon.
ped.set_as_cop(is_cop=True)Toggle cop relationship.
ped.set_relationship_group(group_hash)Assign a relationship group.
ped.task_combat(target, combat_flags=0, threat_flags=16)Make this ped attack target.
ped.clear_tasks()Cancel all current tasks.
ped.set_into(vehicle, seat=-1)Teleport into a vehicle. -1 = driver.
ped.is_shooting / is_jackingRead-only booleans.
ped.is_in_vehicle(vehicle=None)Check membership in any or a specific vehicle.
ped.dismiss() / ped.delete()Mark no-longer-needed, or hard-delete.

Ped also inherits everything from Entity: position, heading, health, exists, is_dead.

Vehicles

Spawning

from gta import world, player, math as gmath

me = player.current()
front = gmath.add(me.ped.position, (0.0, 5.0, 0.0))

car = world.create_vehicle("adder", front, heading=me.ped.heading)
if car:
    car.set_engine(True)
    car.place_on_ground()

Vehicle API

MemberDescription
veh.is_driveableRead-only bool.
veh.passenger_count / max_passengersRead-only ints.
veh.set_engine(on, instantly=True)Start or stop the engine.
veh.place_on_ground()Snap the vehicle on ground.
veh.ped_in_seat(seat)Return a Ped in seat, or None.
veh.delete()Force-delete the vehicle.

Drop the player into the vehicle

player_ped = player.current().ped
player_ped.set_into(car, seat=-1)   # -1 = driver

Entities

Entity is the base class of Ped and Vehicle.

MemberDescription
ent.handleRaw int handle.
ent.existsRead-only bool.
ent.positionGet or set as (x, y, z).
ent.headingGet or set float (degrees).
ent.healthGet or set int.
ent.is_deadRead-only bool.
ent.dismiss()Mark as no-longer-needed.
ent.delete()Force-delete it now.
# You can wrap any handle (e.g. one returned by a native) in Entity
from gta.entity import Entity

def tag_at(handle):
    e = Entity(handle)
    if e.exists:
        gta.log("info", f"entity at {e.position}")

World

Listing entities

from gta import world

peds     = world.get_all_peds()        # list[Ped]
vehicles = world.get_all_vehicles()    # list[Vehicle]

If you only need raw int handles:

import gta

ped_handles = gta.get_all_peds()       # list[int]
veh_handles = gta.get_all_vehicles()

Models & spawning

FunctionDescription
world.load_model(model, timeout_ms=2500)Request a model. Returns the hash on success, 0 on failure.
world.release_model(model)Tell the engine the model is no longer needed.
world.create_ped(model, pos, heading=0.0, ped_type=26)Returns a Ped or None.
world.create_vehicle(model, pos, heading=0.0)Returns a Vehicle or None.

Math & Vectors

Vectors are plain tuples of three floats. gta.math ships small helpers:

from gta import math as gmath

a = (100.0, 200.0, 50.0)
b = (110.0, 200.0, 52.0)

gmath.distance(a, b)              # 3D distance
gmath.distance_2d(a, b)           # XY distance
gmath.length(a)                   # vector length
gmath.add(a, b)                   # tuple sum
gmath.sub(a, b)
gmath.scale(a, 0.5)
gmath.offset_around(a, 5.0, 1.57)   # orbit on XY plane
ℹ️ Vector3
Natives returning vector3 give a namedtuple("Vector3", "x y z"). It is iterable, indexable and unpackable, so it works with gmath helpers transparently.

UI & Notifications

Quick notifications

import gta
from gta import ui

gta.notify("~g~Loaded")
ui.notify("~y~Same as gta.notify")
ui.show_subtitle("Subtitle text", 2500)
ui.show_help_text("Press ~INPUT_CONTEXT~ to interact")

Color codes

CodeColorCodeColor
~r~Red~p~Purple
~g~Green~o~Orange
~b~Blue~y~Yellow
~w~White~n~Newline

Keys & Input

on_key_down receives the Win32 virtual key code plus modifier booleans:

def on_key_down(vk, down, shift, ctrl, alt):
    if not down:
        return

    if vk == 0x70:                 # F1
        gta.notify("F1")
    elif vk == 0x75:               # F6
        gta.notify("F6")
    elif ctrl and vk == ord("R"):
        gta.notify("Ctrl+R")

Common virtual key codes

HexKeyHexKey
0x70F10x76F7
0x71F20x77F8
0x72F30x78F9
0x73F40x79F10
0x74F50x1BEscape
0x75F60x20Space
0x60..0x69Numpad 0..90x6DNumpad minus
0x41..0x5AA..Z (use ord("X"))0x30..0x390..9 (top row)
⚠️ Do not pause in hooks
Do not call gta.wait() from on_start, on_key_down or on_aborted. Set a flag and act in on_tick instead.

Time & Weather

Time and weather natives live in gta.natives.clock and gta.natives.misc.

from gta.natives.clock import SET_CLOCK_TIME, PAUSE_CLOCK
from gta.natives.misc  import SET_WEATHER_TYPE_NOW_PERSIST, SET_TIME_SCALE

# Time
SET_CLOCK_TIME(12, 0, 0)             # noon
PAUSE_CLOCK(True)                  # freeze in-game time

# Weather (string presets)
SET_WEATHER_TYPE_NOW_PERSIST("CLEAR")
SET_WEATHER_TYPE_NOW_PERSIST("RAIN")
SET_WEATHER_TYPE_NOW_PERSIST("THUNDER")
SET_WEATHER_TYPE_NOW_PERSIST("XMAS")        # snow

# Slow-motion: 0.0..1.0, 1.0 is normal speed
SET_TIME_SCALE(0.5)

Explosions

from gta.natives.fire import ADD_EXPLOSION

# ADD_EXPLOSION(x, y, z, type, damage, audible, invisible, cam_shake)
# type: 2 = molotov, 4 = car, 7 = grenade, 12 = fire, 22 = flare, 29 = blimp
def on_key_down(vk, down, shift, ctrl, alt):
    if down and vk == 0x76:   # F7
        from gta import player
        x, y, z = player.current().ped.position
        ADD_EXPLOSION(x, y, z + 2.0, 7, 1.0, True, False, 1.0)

Blips

Blips are managed via gta.natives.hud. There is no high-level wrapper yet.

from gta.natives.hud import (
    ADD_BLIP_FOR_COORD, ADD_BLIP_FOR_ENTITY,
    SET_BLIP_SPRITE, SET_BLIP_COLOUR, SET_BLIP_SCALE,
    SET_BLIP_AS_SHORT_RANGE, SET_BLIP_FLASHES,
    BEGIN_TEXT_COMMAND_SET_BLIP_NAME, ADD_TEXT_COMPONENT_SUBSTRING_PLAYER_NAME,
    END_TEXT_COMMAND_SET_BLIP_NAME, REMOVE_BLIP,
)

_blip = None

def make_blip(x, y, z, name="Marker"):
    blip = ADD_BLIP_FOR_COORD(x, y, z)
    SET_BLIP_SPRITE(blip, 1)         # 1 = standard dot
    SET_BLIP_COLOUR(blip, 1)         # 1 = red
    SET_BLIP_SCALE(blip, 0.85)
    SET_BLIP_AS_SHORT_RANGE(blip, False)
    SET_BLIP_FLASHES(blip, True)
    BEGIN_TEXT_COMMAND_SET_BLIP_NAME("STRING")
    ADD_TEXT_COMPONENT_SUBSTRING_PLAYER_NAME(name)
    END_TEXT_COMMAND_SET_BLIP_NAME(blip)
    return blip

def on_start():
    global _blip
    _blip = make_blip(100.0, 200.0, 25.0, "My place")

def on_aborted():
    global _blip
    if _blip is not None:
        REMOVE_BLIP(_blip)
        _blip = None

Props

Props (barrels, cones, props of any kind) come from gta.natives.object.

from gta import world, player
from gta.entity import Entity
from gta.natives.object import CREATE_OBJECT, DELETE_OBJECT

def spawn_barrel_in_front():
    me = player.current()
    x, y, z = me.ped.position
    # Make sure the model is loaded first
    model = world.load_model("prop_barrel_02a")
    if not model:
        return None
    # CREATE_OBJECT(modelHash, x, y, z, isNetwork, netMissionEntity, dynamic)
    handle = CREATE_OBJECT(model, x + 2.0, y + 2.0, z, False, False, True)
    world.release_model(model)
    return Entity(handle)   # now you can use .position / .heading / .delete()
💡 Find prop names

Browse models on gta-objects.xyz/objects.

Audio

Play UI sounds with gta.natives.audio.

from gta.natives.audio import (
    PLAY_SOUND_FRONTEND, GET_SOUND_ID,
    STOP_SOUND, RELEASE_SOUND_ID,
)

# Common UI sounds
PLAY_SOUND_FRONTEND(-1, "SELECT",    "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "NAV_UP_DOWN", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "ERROR",     "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "CANCEL",    "HUD_FRONTEND_DEFAULT_SOUNDSET", True)

# A sound you can stop later
sid = GET_SOUND_ID()
PLAY_SOUND_FRONTEND(sid, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
# ...later:
STOP_SOUND(sid)
RELEASE_SOUND_ID(sid)

Animations

Play an animation on a Ped. Always request the dictionary first and wait for it.

import gta
from gta import player
from gta.natives.streaming import REQUEST_ANIM_DICT, HAS_ANIM_DICT_LOADED
from gta.natives.task      import TASK_PLAY_ANIM, STOP_ANIM_TASK

def play_anim(dict_name, anim_name):
    REQUEST_ANIM_DICT(dict_name)
    for _ in range(40):              # up to 2 seconds
        if HAS_ANIM_DICT_LOADED(dict_name):
            break
        gta.wait(50)
    ped = player.current().ped.handle
    # TASK_PLAY_ANIM(ped, dict, anim, blendIn, blendOut, duration, flag, rate, lockX, lockY, lockZ)
    TASK_PLAY_ANIM(ped, dict_name, anim_name, 8.0, -8.0, -1, 1, 0.0, False, False, False)

def stop_anim(dict_name, anim_name):
    STOP_ANIM_TASK(player.current().ped.handle, dict_name, anim_name, 3.0)

Cameras

Custom cameras live in gta.natives.cam.

from gta.natives.cam import (
    CREATE_CAM_WITH_PARAMS, RENDER_SCRIPT_CAMS,
    SET_CAM_ACTIVE, DESTROY_CAM, SET_CAM_FOV, SET_CAM_COORD, SET_CAM_ROT,
)

_cam = None

def enable_cam(x, y, z, rx, ry, rz, fov=50.0):
    global _cam
    _cam = CREATE_CAM_WITH_PARAMS("DEFAULT_SCRIPTED_CAMERA",
                                  x, y, z, rx, ry, rz, fov, True, 2)
    SET_CAM_ACTIVE(_cam, True)
    RENDER_SCRIPT_CAMS(True, False, 0, True, False, 0)

def disable_cam():
    global _cam
    RENDER_SCRIPT_CAMS(False, False, 0, True, False, 0)
    if _cam is not None:
        DESTROY_CAM(_cam, False)
        _cam = None

def on_aborted():
    disable_cam()       # always restore gameplay camera

Raycasting

Cast a ray and read the result with gta.natives.shapetest.

from gta import player
from gta.natives.shapetest import (
    START_SHAPE_TEST_RAY, GET_SHAPE_TEST_RESULT,
)

def ray_in_front_of_player(distance=50.0):
    me = player.current().ped
    sx, sy, sz = me.position
    # Quick approx: shoot along the screen forward by stepping heading
    fx = 0.0; fy = 1.0; fz = 0.0   # override with your forward vector if needed
    ex = sx + fx * distance
    ey = sy + fy * distance
    ez = sz + fz * distance

    # flags: -1 = everything, 1 = world, 2 = vehicles, 4 = peds, 8 = objects, 16 = water
    handle = START_SHAPE_TEST_RAY(sx, sy, sz, ex, ey, ez, -1, me.handle, 7)
    # GET_SHAPE_TEST_RESULT returns: status, hit_bool, end_pos, surface_normal, entity_hit
    status, did_hit, end_pos, normal, hit_entity = GET_SHAPE_TEST_RESULT(handle)
    return did_hit, end_pos, hit_entity
ℹ️ Direction vectors
For a real forward vector use gta.natives.entity.GET_ENTITY_FORWARD_VECTOR.

Model loading

Some models are not in memory until you request them.

from gta import world

def spawn_safe(model, pos, heading=0.0):
    h = world.load_model(model)         # request + wait, returns 0 on failure
    if not h:
        gta.notify(f"~r~Model failed: {model}")
        return None
    veh = world.create_vehicle(model, pos, heading)
    world.release_model(model)          # let the engine free it
    return veh

If you prefer the raw layer:

from gta.natives.streaming import (
    REQUEST_MODEL, HAS_MODEL_LOADED,
    IS_MODEL_VALID, IS_MODEL_IN_CDIMAGE,
    SET_MODEL_AS_NO_LONGER_NEEDED,
)
from gta.natives.misc import GET_HASH_KEY

def load_model_raw(name, timeout_ms=2500):
    h = GET_HASH_KEY(name)
    if not IS_MODEL_VALID(h) or not IS_MODEL_IN_CDIMAGE(h):
        return 0
    REQUEST_MODEL(h)
    for _ in range(timeout_ms // 50):
        if HAS_MODEL_LOADED(h):
            return h
        gta.wait(50)
    return 0
⚠️ Free models you load
Always pair load_model / REQUEST_MODEL with release_model / SET_MODEL_AS_NO_LONGER_NEEDED after spawning.

Native Functions

PyloaderV ships 6649 auto-generated native bindings across 45 namespaces. Importing a native is a normal Python import:

from gta.natives.player import PLAYER_PED_ID, GET_PLAYER_WANTED_LEVEL
from gta.natives.entity import GET_ENTITY_COORDS
from gta.natives.misc   import GET_HASH_KEY, SET_TIME_SCALE

def on_tick():
    ped = PLAYER_PED_ID()
    pos = GET_ENTITY_COORDS(ped, True)
    wanted = GET_PLAYER_WANTED_LEVEL(0)
    SET_TIME_SCALE(0.5)

Common namespaces

playerpedvehicleentity weapontaskstreamingmisc hudgraphicscamaudio cutscenenetworkstatsmoney

📚 Native database
Hashes, names and parameter docs live at nativedb.dotindustries.dev/gta5.

Return Types

The wrappers in gta.natives.* already pick the correct return type for you. You only need to specify return_type when you call a native by hash with gta.invoke.

return_typePython valueUse for
"void"NoneNatives that return nothing.
"bool"boolBoolean flags.
"int" / "handle" / "hash"intSigned values, entity handles, hashes.
"uint" / "ulong"intUnsigned values (model hashes for example).
"float"floatFloating-point natives.
"str"str | NoneNatives returning a C string.
"vector3"Vector3(x, y, z)Coordinates and rotations.
import gta

# Same call as PLAYER_PED_ID, but spelled out
ped = gta.invoke(0xD80958FC74E988A6, return_type="handle")
⚠️ Default is "int"
If you forget return_type on a float / vector3 / string native, the result will be wrong.

High-level vs raw

You can mix the two layers freely. The wrappers exist to make common things short, the natives to make everything possible.

Raw natives (verbose)
import gta
from gta.natives.player    import PLAYER_PED_ID
from gta.natives.entity    import (
    GET_ENTITY_COORDS, GET_ENTITY_HEADING)
from gta.natives.misc      import GET_HASH_KEY
from gta.natives.streaming import (
    REQUEST_MODEL, HAS_MODEL_LOADED,
    SET_MODEL_AS_NO_LONGER_NEEDED)
from gta.natives.vehicle   import CREATE_VEHICLE

def spawn_adder():
    ped     = PLAYER_PED_ID()
    coords  = GET_ENTITY_COORDS(ped, True)
    heading = GET_ENTITY_HEADING(ped)
    h       = GET_HASH_KEY("adder")
    REQUEST_MODEL(h)
    while not HAS_MODEL_LOADED(h):
        gta.wait(50)
    veh = CREATE_VEHICLE(h,
        coords.x + 3.0, coords.y, coords.z,
        heading, False, False, False)
    SET_MODEL_AS_NO_LONGER_NEEDED(h)
    return veh
High-level wrappers (idiomatic)
from gta import world, player, math as gmath

def spawn_adder():
    me = player.current()
    pos = gmath.add(me.ped.position, (3.0, 0.0, 0.0))
    car = world.create_vehicle("adder", pos,
                                heading=me.ped.heading)
    if car:
        car.set_engine(True)
        car.place_on_ground()
    return car
💡 When to drop down
Reach for raw natives when:
  • The wrapper does not expose a flag you need.
  • You are calling something niche (network, social club, replay, etc.).
  • You want to inline a single hot call and skip wrapper overhead.

Pip packages

PyloaderV embeds real CPython 3.12, so packages from PyPI work (as long as they support Python 3.12 / Windows x64).

  1. Install Python 3.12 anywhere on your machine (only used as a tool here).
  2. Install the package targeting the embedded runtime:
py -3.12 -m pip install --target "<GTAV>\shvpy_runtime\Lib\site-packages" numpy
py -3.12 -m pip install --target "<GTAV>\shvpy_runtime\Lib\site-packages" requests

Now you can import numpy from any script.

ℹ️ What this looks like
All Python imports happen inside the GTA V process while the game is running. Heavy packages work, but be mindful of frame-time impact in on_tick.

Configuration

Edit PyloaderV.ini next to PyloaderV.asi:

[PyloaderV]

# Folder (relative to GTAV root) where .py scripts live.
ScriptsDir=pyscript

# Optional: absolute path to embedded Python distribution.
# Leave empty to use the runtime sitting next to PyloaderV.asi.
PythonHome=

# Key to force-reload all scripts.
ReloadKey=F9

# Hot-reload scripts when their .py file changes.
AutoReload=false
HotReloadInterval=2

# Print debug notifications in-game.
DebugMode=false

# Logging.
LogEnabled=true
LogMaxSizeKB=1024

# Abort a script if a single tick runs longer than this (ms).
ScriptTimeoutMs=5000
💡 Tip
Set AutoReload=true while developing. PyloaderV will reload your script as soon as you save the file.

Logs & Debugging

Runtime messages are written to PyloaderV.log next to PyloaderV.asi.

import gta

gta.log("debug", "detailed info")
gta.log("info",  "normal message")
gta.log("warn",  "something unusual")
gta.log("error", "something failed")

# print() is also redirected to the log
print("normal print goes to PyloaderV.log too")
ℹ️ Tracebacks
Unhandled exceptions in your callbacks are caught and printed to the log with the full traceback. A bad on_tick won’t kill the rest of your scripts.

Performance

  • Use INTERVAL_MS aggressively. 0 means every frame and is rarely necessary.
  • Common values: 50, 100, 250, 1000.
  • Cache references in on_tick: store me = player.current() instead of calling it twice.
  • Avoid hundreds of native calls per frame.
  • Import only the natives you need.
  • Remove diagnostic / verbose scripts before testing performance.

FAQ & Troubleshooting

My script doesn’t run!

  • Confirm the file is directly under pyscript/.
  • Filenames starting with __ are skipped.
  • Files inside a Lib folder are libraries, not entry points.
  • Check PyloaderV.log: load failures print the full traceback there.

I get “module not found”

  • Imports are from gta import player, not import GTA. PyloaderV is a Python module, not a .NET namespace.
  • Shared modules go in pyscript/Lib/.
  • For pip-installed packages, install them into shvpy_runtime\Lib\site-packages\.

NumPy / requests / external libs?

  • Yes — PyloaderV uses real CPython 3.12, so any Windows-x64 / Python-3.12 wheel works.
  • Install with --target "...\shvpy_runtime\Lib\site-packages" (see the Pip section).

How do I clean up properly?

  • Use on_aborted() to delete blips, dismiss spawned peds/vehicles, restore camera.
  • Track everything you spawn in a list so the cleanup is easy.

Can I use multiple script files?

  • Yes. Every .py directly under pyscript/ is its own script with its own on_start / on_tick.
  • Use __priority__ to control load order if it matters.

Autocomplete in VS Code?

Open for devs (only)/example-project/ in VS Code, install the Python extension, then type gta. in my_script.py.

How do I see “Loaded N script(s)”?

It shows up as a top-left ticker once the game loads. The same line is printed in PyloaderV.log.